
package com.studiofortress.sf.graphics;

import com.sun.opengl.util.j2d.TextRenderer;
import java.awt.Color;
import java.awt.Font;
import java.awt.Polygon;
import java.awt.geom.Rectangle2D;
import javax.media.opengl.GL;
import java.util.HashMap;
import java.util.Map;

/**
 * This is an abstraction layer to simplify making OpenGL calls.
 *
 * Typically the user should never need to create a GraphisGL. Instead it is
 * created by the MainLoop and passed to the World which it is running.
 * 
 * The colour components returned are a float value from 0.0 to 1.0
 * representing no (0.0) or full (1.0) colour. Unless you want a Color object,
 * it's advised to use the indevidual get color component methods instead of
 * getColor.
 *
 * The draw versions of a method will draw an outline of what you are drawing.
 * For example drawRect is an outline of a rectangle, drawPolygon is an outline
 * of the given polygon. The fill versions draws the stated item as a filled
 * shape.
 *
 * With respect to the oval painting methods, a true oval is never drawn.
 * Instead a polygon. The number of segments depicts how many sides the oval
 * will have, however it will slow down rendering time.
 *
 * @author Joseph Lenton
 */
public class GraphicsGL
{
    // the default number of points on an oval
    private static final int DEFAULT_OVAL_SEGMENTS = 24;
    private static final int DEFAULT_FONT_SIZE = 12;

    private GL gl;

    /* This is the current text renderer in use. When set to null then it should
     * be retrieved from the textRenderers. */
    private TextRenderer textRenderer;
    
    /* A map of all of the TextRenderers built using this */
    private Map<Font, TextRenderer> textRenderers;
    
    /* The current font in use. */
    private Font textFont;
    
    private float red;
    private float green;
    private float blue;
    private float alpha;

    // These are for the transform and undoTransform methods.
    // It seems when I pop the matrix it undoes the orthographic positioning.
    private float undoTransformX;
    private float undoTransformY;
    private float undoTransformRotation;

//    private MatrixStack matrix; // TODO

    /**
     * Creates a new GLDraw object that wraps the given GL object.
     * This allows you to use OpenGL for drawing common Drawable primitives.
     * @param gl A OpenGL object to wrap. Cannot be null.
     */
    public GraphicsGL( GL gl )
    {
        if (gl == null) {
            throw new IllegalArgumentException("GL cannot be null.");
        }
        
//        this.matrix = new MatrixStack(); // TODO
        this.gl = gl;
        this.textRenderers = new HashMap<Font, TextRenderer>();
        this.textRenderer  = null;
        this.textFont = new Font(null, Font.PLAIN, DEFAULT_FONT_SIZE);

        // resets the colour each time this is setup
        setColor(255, 255, 255, 255);
        
        applyMatrix();
    }
    
    private final TextRenderer getTextRenderer()
    {
        TextRenderer textR = textRenderers.get( textFont );
        
        if ( textR == null ) {
            textR = new TextRenderer( textFont );
            textRenderers.put( textFont, textR );
        }
        
        return textR;
    }
    
    /**
     * Applys all of the currently stored matrix operations.
     */
    private final void applyMatrix()
    {
        // TODO
    }

    /**
     * Stops rendering and calls to send all render instructions to the graphics card.
     */
    public void flush()
    {
        gl.glFlush();
    }

    public void drawTextureScaled(final GLTexture texture, final float x, final float y, final float scale)
    {
        drawTextureScaled( texture, x, y, scale, scale );
    }

    public void drawTextureScaled(final GLTexture texture, final float x, final float y, final float scale, final boolean centred)
    {
        drawTextureScaled( texture, x, y, scale, scale, centred );
    }

    public void drawTextureScaled(final GLTexture texture, final float x, final float y, final float xScale, final float yScale)
    {
        drawTextureScaled( texture, x, y, xScale, yScale, false );
    }

    public void drawTextureScaled(final GLTexture texture, final float x, final float y, final float xScale, final float yScale, final boolean centred)
    {
        drawTexture( texture, x, y, texture.getWidth()*xScale, texture.getHeight()*yScale, centred );
    }
    
    /**
     * Draws the given texture at the given location. The x and y values are the
     * bottom left corner of the texture being drawn.
     * @param texture The texture to draw.
     * @param x The x co-ordinate of the bottom left corner of where to draw the texture.
     * @param y The y co-ordinate of the bottom left corner of where to draw the texture.
     */
    public void drawTexture(final GLTexture texture, final float x, final float y)
    {
        drawTexture(texture, x, y, false);
    }

    /**
     * Draws the given texture at the given location. If centred then the
     * texture is drawn around the given x and y values. Otherwise they are the
     * bottom left corner of the texture being drawn.
     * @param texture The texture to draw.
     * @param x The x co-ordinate of the bottom left corner of where to draw the texture.
     * @param y The y co-ordinate of the bottom left corner of where to draw the texture.
     * @param centred True if the texture should be centred around the x and y co-ordinates given, otherwise false.
     */
    public void drawTexture(final GLTexture texture, float x, float y, final boolean centred)
    {
        drawTexture( texture, x, y, texture.getWidth(), texture.getHeight(), centred );
    }

    /**
     * Draws the given texture but scaled to the given width and height.
     * @param texture The texture to draw, cannot be null.
     * @param x The x co-ordinate of the bottom left corner of the texture.
     * @param y The y co-ordinate of the bottom left corner of the texture.
     * @param width The width that the texture will appear when drawn.
     * @param height The height of the texture on the screen when drawn.
     */
    public void drawTexture(final GLTexture texture, float x, float y, float width, float height)
    {
        drawTexture( texture, x, y, width, height, false );
    }

    /**
     * Draws the given texture rotated around it's bottom left corner at the scale given.
     * @param texture
     * @param x
     * @param y
     * @param width
     * @param height
     * @param rotation
     */
    public void drawTexture(final GLTexture texture, float x, float y, float width, float height, float rotation)
    {
        transform(x, y, rotation);
        drawTexture( texture, 0, 0, width, height);
        undoTransform();
    }

    /**
     *
     * @param texture
     * @param x
     * @param y
     * @param width
     * @param height
     * @param rotation
     */
    public void drawTexture(final GLTexture texture, float x, float y, float width, float height, boolean centered, float rotation)
    {
        transform(x, y, rotation);
        drawTexture( texture, 0, 0, width, height, centered);
        undoTransform();
    }
    
    /**
     * Draws the given texture at the given location, scaled to the given width
     * height and midhandled or not depending on the value of 'centred'.
     * @param texture The texture to draw.
     * @param x If midhandled, then this is the centre of the texture. Otherwise it is the bottom left-corner.
     * @param y If midhandled, then this is the centre of the texture. Otherwise it is the bottom left-corner.
     * @param width The width of the texture when drawn.
     * @param height The height of this texture when drawn.
     * @param centred True if this texture should be midhandled, false if not.
     */
    public void drawTexture(final GLTexture texture, float x, float y, float width, float height, final boolean centred)
    {
        texture.enable();
        texture.bind();
        gl.glBegin( GL.GL_QUADS );
        
        if ( centred ) {
            x -= width/2;
            y -= height/2;
        }
        
        final float xWidth = x+width;
        final float yHeight = y+height;

        final GLTextureInner innerTex = texture.getInternalTexture();

        // bottom left
        gl.glTexCoord2f(innerTex.getLeftCoord(), innerTex.getBottomCoord());
        gl.glVertex2f(x, y);

         // bottom right
        gl.glTexCoord2f(innerTex.getRightCoord(), innerTex.getBottomCoord());
        gl.glVertex2f(xWidth, y);

         // top right
        gl.glTexCoord2f(innerTex.getRightCoord(), innerTex.getTopCoord());
        gl.glVertex2f(xWidth, yHeight);
        
         // top left
        gl.glTexCoord2f(innerTex.getLeftCoord(), innerTex.getTopCoord());
        gl.glVertex2f(x, yHeight);

        gl.glEnd();
        texture.disable();
    }

    /**
     * Draws a texture at the given x and y co-ordinate, and rotated by the
     * given amount. The x and y co-ordinates correspond to the bottom left
     * corner of where to draw the texture.
     * @param texture The texture to draw.
     * @param x The bottom left x co-ordinate of where to draw the rectangle.
     * @param y The bottom left y co-ordinate of where to draw the rectangle.
     * @param rotation The amount to rotate the texture by, in degrees.
     */
    public void drawTexture(final GLTexture texture, float x, float y, float rotation)
    {
        transform(x, y, rotation);
        drawTexture(texture, 0, 0);
        undoTransform();
    }
    
    /**
     * This draws the given texture at the given x and y location. The centred
     * boolean states if the x and y location is the bottom left corner of the
     * texture (when it is false) or the centre of the texture (when it is true).
     * @param texture The texture to draw, cannot be null.
     * @param x Where to draw the texture along the x axis.
     * @param y Where to draw the texture along the y axis.
     * @param centred True if it will be centred around the given x and y co-ordinates, false if not.
     * @param rotation The amount to rotate the texture, clockwise and in degrees.
     */
    public void drawTexture(final GLTexture texture, float x, float y, final boolean centred, float rotation)
    {
        transform(x, y, rotation);
        drawTexture(texture, 0, 0, centred);
        undoTransform();
    }

    /**
     * Rotates the GraphicsGL by the given amount for all future operations.
     * @param rotation The amount to rotate the graphics by in degrees.
     */
    public void rotate(final float rotation)
    {
        gl.glRotatef(rotation, 0.0f, 0.0f, 1.0f);
    }

    /**
     * Translates the GraphicsGL by the given X and Y amount.
     * @param x The amount to move the GraphicsGL along by on the x-axis.
     * @param y The amount to move the GraphicsGL along by on the y-axis.
     */
    public void translate(final float x, final float y)
    {
        gl.glTranslatef(x, y, 0.0f);
    }

    /**
     * A helper function for applying rotations to an existing graphics operation.
     * This will push two matrixes onto the GL pipelien which should be later
     * removed by calling 'undoTransform'.
     *
     * The rotation is clockwise and in degrees.
     *
     * @param x The GL pipeline will be moved so this co-ordinate is it's centre.
     * @param y The GL pipeline will be moved so this co-ordinate is it's centre.
     * @param rotation The pipeline will be rotated by this amount, in degrees.
     */
    protected void transform(final float x, final float y, float rotation)
    {
        // invert so it rotates clockwise
        rotation = -rotation;

        gl.glTranslatef(x, y, 0.0f);
        gl.glRotatef(rotation, 0.0f, 0.0f, 1.0f);

        // store for the undo
        undoTransformX = x;
        undoTransformY = y;
        undoTransformRotation = rotation;
    }

    /**
     * Undo's the transformations from the last call to transform.
     * This should not be called if there was no transform.
     */
    protected void undoTransform()
    {
        gl.glRotatef(-undoTransformRotation, 0.0f, 0.0f, 1.0f);
        gl.glTranslatef(-undoTransformX, -undoTransformY, 0.0f);
    }

    /**
     * @return The underlying GL object that this GLDraw wraps.
     */
    public GL getGL()
    {
        return gl;
    }

    public void setColor(final Color color)
    {
        setColor(
                color.getRed() / 255.0f,
                color.getGreen() / 255.0f,
                color.getBlue() / 255.0f,
                color.getAlpha() / 255.0f);
    }

    public void setColor(
            final int red, final int green, final int blue)
    {
       setColor(red / 255.0f, green / 255.0f, blue / 255.0f, 1f);
    }

    public void setColor(
            final int red, final int green, final int blue, final int alpha)
    {
        setColor(red / 255.0f, green / 255.0f, blue / 255.0f, alpha / 255.0f);
    }

    public void setColor(
            final float red, final float green, final float blue)
    {
        setColor( red, green, blue, 1f );
    }

    public void setColor(
            final float red, final float green, final float blue, final float alpha)
    {
        gl.glColor4f(red, green, blue, alpha);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.alpha = alpha;
    }

    public void setAlpha(float alpha)
    {
        setColor( red, green, blue, alpha );
    }
    
    /**
     * It's advised to use the indevidual colour component methods (getRed,
     * getGreen, getBlue and getAlpha) instead of this one. Especially if you
     * only want one colour.
     * @return The colour currently used when drawing.
     */
    public Color getColor()
    {
        return new Color( red, green, blue, alpha );
    }

    /**
     * @return The value used for the red component when drawing.
     */
    public float getRed()
    {
        return red;
    }

    /**
     * @return The value used for the green component when drawing.
     */
    public float getGreen()
    {
        return green;
    }

    /**
     * @return The value used for the blue component when drawing.
     */
    public float getBlue()
    {
        return blue;
    }

    /**
     * @return The value used for the alpha component when drawing.
     */
    public float getAlpha()
    {
        return alpha;
    }

    public void drawRect(
            final float x, final float y, final float width, final float height, float rotation)
    {
        transform(x, y, rotation);
        drawRect(0, 0, width, height);
        undoTransform();
    }

    public void drawRect(
            final float x, final float y, final float width, final float height)
    {
        gl.glBegin(GL.GL_LINE_LOOP);

        final float xWidth = x+width;
        final float yHeight = y+height;

        gl.glVertex2f(x, y); // bottom left
        gl.glVertex2f(xWidth, y); // bottom right
        gl.glVertex2f(xWidth+1, yHeight); // top right
        gl.glVertex2f(x, yHeight); // top left

        gl.glEnd();
    }

    public void fillRect(
            final float x, final float y, final float width, final float height, float rotation)
    {
        transform(x, y, rotation);
        fillRect(0, 0, width, height);
        undoTransform();
    }

    public void fillRect(
            final float x, final float y, final float width, final float height)
    {
        final float xWidth = x+width;
        final float yHeight = y+height;

        gl.glBegin(GL.GL_QUADS);

        gl.glVertex2f(x, y); // bottom left
        gl.glVertex2f(xWidth, y); // bottom right
        gl.glVertex2f(xWidth, yHeight); // top right
        gl.glVertex2f(x, yHeight); // top left

        gl.glEnd();
    }

    /**
     * Draws a line from x1,y1 to x2,y2
     * @param x1 The x co-ordinate of the start of the line.
     * @param y1 The y co-ordinate of the start of the line.
     * @param x2 The x co-ordinate of the end of the line.
     * @param y2 The y co-ordinate of the end of the line.
     */
    public void drawLine(
            final float x1, final float y1, final float x2, final float y2)
    {
        gl.glBegin(GL.GL_LINES);

        gl.glVertex2f(x1, y1);
        gl.glVertex2f(x2, y2);

        gl.glEnd();
    }

    public void drawOval(
            final float x, final float y, final float width, final float height, final float rotation)
    {
        transform(x, y, rotation);
        drawOval(0, 0, width, height);
        undoTransform();
    }

    /**
     * Draws an oval where the given x and y values are the bottom-left corner
     * of the oval. It stretches up and to the right of the screen by the given
     * width or height. A negative width or height will make it stretch in the
     * opposite direction.
     * @param x
     * @param y
     * @param width
     * @param height
     */
    public void drawOval(
            final float x, final float y, final float width, final float height)
    {
        drawOval(x, y, width, height, DEFAULT_OVAL_SEGMENTS);
    }

    public void fillOval(
            final float x, final float y, final float width, final float height, final float rotation)
    {
        transform(x, y, rotation);
        fillOval(0, 0, width, height);
        undoTransform();
    }

    /**
     * Fills an oval using the default number of points (24).
     * @param x
     * @param y
     * @param width
     * @param height
     */
    public void fillOval(
            final float x, final float y, final float width, final float height)
    {
        fillOval(x, y, width, height, DEFAULT_OVAL_SEGMENTS);
    }

    public void drawOval(
            final float x, final float y,
            final float width, final float height,
            final int segments,
            final float rotation)
    {
        transform(x, y, rotation);
        drawOval(0, 0, width, height, segments);
        undoTransform();
    }

    /**
     *
     * @param x
     * @param y
     * @param width
     * @param height
     * @param segments
     */
    public void drawOval(
            final float x, final float y,
            final float width, final float height,
            final int segments)
    {
        gl.glBegin(GL.GL_LINE_LOOP);

        final float halfWidth = width / 2.0f;
        final float halfHeight = height / 2.0f;
        final float centreX = x+halfWidth;
        final float centreY = y + halfHeight;

        float firstX = x+width;
        float firstY = centreY;

        final double angleIncrement = Math.toRadians(360/segments);
        double angle = 0;

        gl.glVertex2f(firstX, firstY);
        for (int i = 0; i < segments; i++) {
            angle += angleIncrement;
            final double secondX = centreX + halfWidth*Math.cos(angle);
            final double secondY = centreY + halfHeight*Math.sin(angle);

            gl.glVertex2d(secondX, secondY);
        }

        gl.glEnd();
    }

    public void fillOval(
            final float x, final float y,
            final float width, final float height,
            final int segments,
            final float rotation)
    {
        transform(x, y, rotation);
        fillOval(0, 0, width, height, segments);
        undoTransform();
    }

    public void fillOval(
            final float x, final float y,
            final float width, final float height,
            final int segments)
    {
        gl.glBegin(GL.GL_TRIANGLE_FAN);

        final float halfWidth  = width  / 2.0f;
        final float halfHeight = height / 2.0f;
        final float sourceX = x+width;
        final float centreX = x+halfWidth;
        final float centreY = y + halfHeight;

        final double angleIncrement = Math.toRadians(360/segments);
        double angle = angleIncrement;

        double firstX = centreX + halfWidth*Math.cos(angle);
        double firstY = centreY + halfHeight*Math.sin(angle);

        final double secondX = sourceX;
        final double secondY = centreY;

        gl.glVertex2d(firstX, firstY);
        gl.glVertex2d(secondX, secondY);
        for (int i = 0; i < segments-2; i++) {
            angle += angleIncrement;
            final double nextX = centreX + halfWidth*Math.cos(angle);
            final double nextY = centreY + halfHeight*Math.sin(angle);

            gl.glVertex2d(nextX, nextY);
        }
        // we go back to the second point we made to finish the oval
        gl.glVertex2d(secondX, secondY);

        gl.glEnd();
    }

    /**
     * Draws a single pixel at the point given.
     * @param x The x co-ordinate of the point.
     * @param y The y co-ordinate of the pixel to draw.
     */
    public void fillPixel(final float x, final float y)
    {
        gl.glBegin( GL.GL_POINTS );
        gl.glVertex2f( x, y );
        gl.glEnd();
    }

    /**
     * Renders the outling of a triangle to the screen.
     * The three sets of points relate to the three corners of the Triangle.
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @param x3
     * @param y3
     */
    public void drawTriangle(
            final float x1, final float y1,
            final float x2, final float y2,
            final float x3, final float y3)
    {
        gl.glBegin(GL.GL_LINE_LOOP);

        gl.glVertex2f(x1, y1);
        gl.glVertex2f(x2, y2);
        gl.glVertex2f(x3, y3);

        gl.glEnd();
    }

    /**
     * Draws and fills a triangle using the three co-ordinates given. They
     * correspond to each of the triangles three corners.
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @param x3
     * @param y3
     */
    public void fillTriangle(
            final float x1, final float y1,
            final float x2, final float y2,
            final float x3, final float y3)
    {
        gl.glBegin(GL.GL_TRIANGLES);
        
        gl.glVertex2f(x1, y1);
        gl.glVertex2f(x2, y2);
        gl.glVertex2f(x3, y3);

        gl.glEnd();
    }

    /**
     * Draws the given string translated by the given co-ordinates.
     * @param string The string to draw, cannot be null.
     * @param x
     * @param y
     * @param rotation 
     */
    public void drawString(
            final String string, final float x, final float y, final float rotation)
    {
        transform(x, y, rotation);
        drawString(string, 0, 0);
        undoTransform();
    }

    public void drawString(
            final String string, final float x, final float y)
    {
        drawString(string, x, y, false);
    }

    public void drawString(
            final String string, final float x, final float y, final boolean centred, final float rotation)
    {
        transform(x, y, rotation);
        drawString(string, 0, 0, centred);
        undoTransform();
    }

    public void drawString(final String string, final float x, final float y, boolean centred)
    {
        if ( textRenderer == null ) {
            textRenderer = getTextRenderer();
        }

        textRenderer.begin3DRendering();
        textRenderer.setColor( red, green, blue, alpha );

        if (centred) {
            Rectangle2D bounds = textRenderer.getBounds(string);
            textRenderer.draw(string,
                    (int)(x-bounds.getWidth()/2), (int)(y-bounds.getHeight()/2));
        } else {
            textRenderer.draw(string, (int)x, (int)y);
        }

        textRenderer.flush();
        textRenderer.end3DRendering();
    }
    
    /**
     * @param font The font to use when drawing strings. Cannot be null.
     */
    public void setFont(Font font)
    {
        if (font == null) {
            throw new IllegalArgumentException("The given font cannot be null.");
        }
        
        textRenderer = null;
        
        textFont = font;
    }
    
    /**
     * @return The font currently used for drawing Strings.
     */
    public Font getFont()
    {
        return textFont;
    }

    /**
     * @param shape Draws an outline of this shape, cannot be null.
     */
    public void draw(final Polygon shape)
    {
        draw( shape, 0, 0 );
    }

    /**
     * The given shape is translated by the given x and y values when drawn.
     * @param shape Draws an outline of this shape, cannot be null.
     * @param x The amount to translate by along the x-axis.
     * @param y The amount to translate by along the y-axis.
     */
    public void draw(final Polygon shape, final float x, final float y)
    {
        final boolean translate = x != 0 || y != 0;
        if (translate) {
            gl.glTranslated(x, y, 0);
        }

        gl.glBegin(GL.GL_LINE_LOOP);

        for (int i = 0; i < shape.npoints; i++) {
            gl.glVertex2i( shape.xpoints[i], shape.ypoints[i] );
        }

        gl.glEnd();

        if (translate) {
            gl.glTranslatef(-x, -y, 0);
        }
    }

    /**
     * @param shape Renders this shape as a filled polygon shape.
     */
    public void fill(final Polygon shape)
    {
        gl.glBegin(GL.GL_POLYGON);

        for (int i = 0; i < shape.npoints; i++) {
            gl.glVertex2i( shape.xpoints[i], shape.ypoints[i] );
        }
        
        gl.glEnd();
    }

    /**
     * When drawn this will be translated by the given x and y values.
     * @param shape Renders this shape filled in as a whole polygon.
     * @param x The amount to translate by along the x-axis.
     * @param y The amount to translate by along the y-axis.
     */
    public void fill(final Polygon shape, final float x, final float y)
    {
        gl.glPushMatrix();
        gl.glTranslatef(x, y, 0.0f);

        fill(shape);

        gl.glPopMatrix();
    }

    public void fill(final Polygon shape, final float rotation)
    {
        gl.glPushMatrix();
        gl.glRotatef(-rotation, 0.0f, 0.0f, 1.0f);

        fill(shape);

        gl.glPopMatrix();
    }

    public void fill(final Polygon shape, final float x, final float y, final float rotation)
    {
        transform(x, y, rotation);
        fill(shape);
        undoTransform();
    }

    /**
     * Renders the given display list to the screen. Although this is a draw
     * method, the display list decides what is rendered to the screen (i.e. if
     * it's an outline or filled).
     * When drawn you can presume that the GL pipeline is not currently
     * rendering. The given GLDisplayList will need to call glBegin within it.
     * @param glList The GLDisplayList to draw.
     * @param x Translates the given display list along the x-axis by this amount.
     * @param y Translates the given display list along the y-axis by this amount.
     */
    public void draw(final GLDisplayList glList, final float x, final float y)
    {
        if (!glList.hasDisplaylist()) {
            glList.record(gl);
        }
        
        gl.glPushMatrix();
        gl.glTranslatef( x, y, 0 );
        gl.glCallList( glList.getDisplayList() );
        gl.glPopMatrix();
    }

    /**
     * @param text
     * @return The size of the given text when rendered.
     */
    public Rectangle2D getStringSize(CharSequence text)
    {
        if (text == null) {
            throw new IllegalArgumentException("The given text cannot be null.");
        }

        return getTextRenderer().getBounds( text );
    }
}
